package com.guo.redis.business.hello.service.impl;

import com.guo.redis.business.hello.service.IDistributeLockService;
import com.guo.redis.business.hello.service.IStockCallback;
import com.guo.redis.business.hello.service.StockCounterService;
import com.guo.redis.config.lock.LockValueContainer;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.connection.ReturnType;
import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;

import javax.annotation.Resource;
import java.io.Serializable;
import java.util.concurrent.TimeUnit;
/**
 * 模拟库存服务实现，使用 Redis 分布式锁
 */
@Service("distributeLockIncrementStockCounterServiceImpl")
public class DistributeLockIncrementStockCounterServiceImpl implements StockCounterService {

    /**
     * 库存还未初始化
     */
    public static final long UNINITIALIZED_STOCK = -3L;

    /**
     * Redis 客户端
     */
    @Autowired
    private RedisTemplate<String, Serializable> redisTemplate;

    @Resource(name="redisDistributeLockServiceImpl")
    private IDistributeLockService distributeLockService;

    /**
     * 执行扣库存的脚本
     */
    public static final String STOCK_LUA;
    static {
        /**
         *
         * @desc 扣减库存Lua脚本
         * 库存（stock）-1：表示不限库存
         * 库存（stock）0：表示没有库存
         * 库存（stock）大于0：表示剩余库存
         *
         * @params 库存key
         * @return
         *      -3:库存未初始化
         * 		-2:库存不足
         * 		-1:不限库存
         * 		大于等于0:剩余库存（扣减之后剩余的库存）,直接返回-1
         */
        StringBuilder sb = new StringBuilder();
        // exists 判断是否存在KEY，如果存在返回1，不存在返回0
        sb.append("if (redis.call('exists', KEYS[1]) == 1) then");
        // get 获取KEY的缓存值，tonumber 将redis数据转成 lua 的整形
        sb.append("    local stock = tonumber(redis.call('get', KEYS[1]));");
        sb.append("    local num = tonumber(ARGV[1]);");
        // 如果拿到的缓存数等于 -1，代表改商品库存是无限的，直接返回1
        sb.append("    if (stock == -1) then");
        sb.append("        return -1;");
        sb.append("    end;");
        // incrby 特性进行库存的扣减
        sb.append("    if (stock >= num) then");
        sb.append("        return redis.call('incrby', KEYS[1], 0-num);");
        sb.append("    end;");
        sb.append("    return -2;");
        sb.append("end;");
        sb.append("return -3;");
        STOCK_LUA = sb.toString();
    }

    @Override
    public Object getStockNumbers(String stockKey) {
        return redisTemplate.opsForValue().get(stockKey);
    }

    @Override
    public long countDownStock(String stockKey, int num, IStockCallback stockCallback) {
        /*
        获取库存，先查询用LUA 脚本获取
            S1 返回内容判定
                库存减成功，直接返回
                库存为-1，表示不限制库存？
                库存为-2，表示库存不足
                库存为-3，表示库存没有初始化
         */


        long l = countDownStock(stockKey, num);
        if( l == UNINITIALIZED_STOCK){// 如果返回的是 -3， 未初始化，需要先初始化
            String value = LockValueContainer.getInstance().getValue();
            if(distributeLockService.tryLock(stockKey, value, 10, TimeUnit.SECONDS, 2000)){
                l = countDownStock(stockKey, num);
                // double-check 确保 多线程更新时，出现重复覆盖问题
                if( l == UNINITIALIZED_STOCK){
                    stockCallback.initStockAmount(stockKey);
                    l = countDownStock(stockKey, num);
                }
            }
        }
        return l;
    }

    private long countDownStock(String stockKey, int num) {
        byte[] keyBytes = stockKey.getBytes();
        byte[] argsBytes = Integer.toString(num).getBytes();

        return redisTemplate.execute((RedisCallback<Long>) redisConnection -> {

            //集群结构  获取 原生连接对象， 执行 LUA 脚本
//                if (nativeConnection instanceof LettuceClusterConnection) {
//                    return ((LettuceClusterConnection) nativeConnection).scriptingCommands().eval(STOCK_LUA.getBytes(), ReturnType.INTEGER, 1,
//                            keyBytes, argsBytes);
//                }
            //非集群结构  获取 原生连接对象， 执行 LUA 脚本
            return redisConnection.scriptingCommands().eval(STOCK_LUA.getBytes(), ReturnType.INTEGER, 1,
                    keyBytes, argsBytes);
        });
    }

    @Override
    public void initStock(String stockKey, long expire, long amount) {
        redisTemplate.opsForValue().set(stockKey, amount, expire, TimeUnit.SECONDS);
    }
}
